package uk.ac.lkl.common.associator;



import java.util.*;

import uk.ac.lkl.common.associator.event.AssociatorEvent;
import uk.ac.lkl.common.associator.event.AssociatorListener;

import uk.ac.lkl.common.util.ds.AbstractNotifyingList;
import uk.ac.lkl.common.util.ds.NotifyingList;

import uk.ac.lkl.common.util.ds.event.ListAdapter;
import uk.ac.lkl.common.util.ds.event.ListEvent;
import uk.ac.lkl.common.util.ds.event.ListListener;



/**
 * Associates instances of one class with instances of another.
 * 
 * This class maintains a map of links from element of type L to
 * elements of type R.
 * 
 * @author Darren Pearce
 * @version $Revision: 620 $
 * @version $Date: 2008-02-20 00:46:53 +0000 (Wed, 20 Feb 2008) $
 * 
 */
public class Associator<L, A, R> {

    private ArrayList<AssociatorListener<L, A, R>> listeners = new ArrayList<AssociatorListener<L, A, R>>();

    private HashMap<L, ArrayList<R>> leftToRightMap;

    private HashMap<R, ArrayList<L>> rightToLeftMap;

    private HashMap<AssociatorLink, A> annotationMap;

    private AbstractNotifyingList<L> leftList;

    private AbstractNotifyingList<R> rightList;

    /**
     * A simple interface to factor out the swapping of arguments
     * needed when processing element removals.
     * 
     */
    private interface AssociatorNotifier<S, D> {

        public void processLink(S source, D destination);
    };

    /**
     * The listener to the left collection.
     * 
     * This uses a notifier to call notifyAssociationRemoved with the
     * correct (same) argument order.
     * 
     */
    private ListListener<L> leftListener = new ListAdapter<L>() {

        public void elementRemoved(ListEvent<L> e) {
            processElementRemoved(e,
                                  leftToRightMap,
                                  rightToLeftMap,
                                  new AssociatorNotifier<L, R>() {

                                      public void processLink(L left, R right) {
                                          removeAssociation(left, right);
                                      }
                                  });
        }
    };

    /**
     * The listener to the right collection.
     * 
     * Note that this uses a notifier to ensure the correct argument
     * order for link removal event processing.
     * 
     */
    private ListListener<R> rightListener = new ListAdapter<R>() {

        public void elementRemoved(ListEvent<R> e) {
            processElementRemoved(e,
                                  rightToLeftMap,
                                  leftToRightMap,
                                  new AssociatorNotifier<R, L>() {

                                      public void processLink(R right, L left) {
                                          removeAssociation(left, right);
                                      }
                                  });
        }
    };


    public Associator(
                      AbstractNotifyingList<L> leftCollection,
                      AbstractNotifyingList<R> rightCollection) {
        this.leftList = leftCollection;
        this.rightList = rightCollection;
        leftCollection.addListListener(leftListener);
        rightCollection.addListListener(rightListener);
        leftToRightMap = new HashMap<L, ArrayList<R>>();
        rightToLeftMap = new HashMap<R, ArrayList<L>>();
        annotationMap = new HashMap<AssociatorLink, A>();
    }


    private <S, D> void processElementRemoved(ListEvent<S> e,
                                              HashMap<S, ArrayList<D>> outLinksMap,
                                              HashMap<D, ArrayList<S>> inLinksMap,
                                              AssociatorNotifier<S, D> notifier) {
        S element = e.getElement();
        // note: remove and return removed ArrayList.
        ArrayList<D> outLinks = outLinksMap.remove(element);
        if (outLinks == null)
            return;

        for (D destination : outLinks) {
            ArrayList<S> inLinks = inLinksMap.get(destination);
            inLinks.remove(element);
            notifier.processLink(element, destination);
        }
    }


    public void addAssociation(int leftIndex, int rightIndex) {
        addAssociation(leftList.get(leftIndex), rightList.get(rightIndex));
    }


    public void addAssociation(int leftIndex, A annotation, int rightIndex) {
        addAssociation(leftList.get(leftIndex),
                       annotation,
                       rightList.get(rightIndex));
    }


    public void addAssociation(L left, R right) {
        addAssociation(left, null, right);
    }


    public boolean addAssociation(L left, A annotation, R right) {
        // only allow if left is a member of leftCollection and right
        // a member of rightCollection

        // prevent duplicate associations
        ArrayList<R> rights = leftToRightMap.get(left);
        if (rights != null && rights.contains(right))
            return false;

        link(leftToRightMap, left, right);
        link(rightToLeftMap, right, left);

        // annotation may be null (which is fine)
        annotationMap.put(new AssociatorLink(left, right), annotation);
        notifyAssociationAdded(left, annotation, right);
        return true;
    }


    public void setAnnotation(L leftElement, R rightElement, A annotation) {
        // need to check that edge exists before annotate...
        boolean added = addAssociation(leftElement, annotation, rightElement);
        if (!added) {
            // edge must already exist
            // todo: stop creation of lots of instances of Link
            // duplicated work with addAssociation
            annotationMap.put(new AssociatorLink(leftElement, rightElement), annotation);
        }
    }


    // todo: distinguish between null annotation and no link
    // existing??
    public A getAnnotation(L leftElement, R rightElement) {
        return annotationMap.get(new AssociatorLink(leftElement, rightElement));
    }


    // todo: use multimap for this
    private <S, D> void link(HashMap<S, ArrayList<D>> map,
                             S source,
                             D destination) {
        ArrayList<D> destinations = map.get(source);
        if (destinations == null) {
            destinations = new ArrayList<D>();
            map.put(source, destinations);
        }
        destinations.add(destination);
    }


    public void removeAssociation(L left, R right) {
        A annotation = getAnnotation(left, right);
        unlink(leftToRightMap, left, right);
        unlink(rightToLeftMap, right, left);
        notifyAssociationRemoved(left, annotation, right);
    }


    // todo: use multimap for this
    private <S, D> void unlink(HashMap<S, ArrayList<D>> map,
                               S source,
                               D destination) {
        ArrayList<D> destinations = map.get(source);
        if (destinations == null)
            return;
        destinations.remove(destination);
    }


    public ArrayList<R> getRightAssociations(L left) {
        return leftToRightMap.get(left);
    }


    public ArrayList<L> getLeftAssociations(R right) {
        return rightToLeftMap.get(right);
    }


    public AbstractNotifyingList<L> getLeftList() {
        return leftList;
    }


    public Class<L> getLeftClass() {
        return leftList.getElementClass();
    }


    public AbstractNotifyingList<R> getRightList() {
        return rightList;
    }


    public Class<R> getRightClass() {
        return rightList.getElementClass();
    }


    private void notifyAssociationAdded(L left, A annotation, R right) {
        AssociatorEvent<L, A, R> e = new AssociatorEvent<L, A, R>(this, left,
                                                                  annotation,
                                                                  right);
        for (AssociatorListener<L, A, R> listener : listeners)
            listener.associationAdded(e);
    }


    private void notifyAnnotationChanged(L left, A annotation, R right) {
        AssociatorEvent<L, A, R> e = new AssociatorEvent<L, A, R>(this, left,
                                                                  annotation,
                                                                  right);
        for (AssociatorListener<L, A, R> listener : listeners)
            listener.annotationChanged(e);
    }


    private void notifyAssociationRemoved(L left, A annotation, R right) {
        AssociatorEvent<L, A, R> e = new AssociatorEvent<L, A, R>(this, left,
                                                                  annotation,
                                                                  right);
        for (AssociatorListener<L, A, R> listener : listeners)
            listener.associationRemoved(e);
    }


    public void addAssociatorListener(AssociatorListener<L, A, R> listener) {
        listeners.add(listener);
    }


    public void removeAssociatorListener(AssociatorListener<L, A, R> listener) {
        listeners.remove(listener);
    }


    public String toString() {
        String result = "l2r: " + leftToRightMap + ", " + "r2l: "
                + rightToLeftMap;
        return result;
    }
}
